using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;

using Mono.Options;

namespace Stubify
{
    class Program
    {
        static int Main(string[] args)
        {
            Program p = new Program();
            return p.Run(args);
        }

        private string _outputPath;
        private string _assemblyPath;
        private string _namespaceName = "Stubs";
        private readonly List<string> _typeNames = new List<string>();

        private int Run(IEnumerable<string> args)
        {
            try
            {
                OptionSet options = new OptionSet().
                    Add("assembly=", "path to assembly containing types", delegate(string value) { _assemblyPath = value; }).
                    Add("out=", "path to write stubs to", delegate(string value) { _outputPath = value; }).
                    Add("namespace=", "namespace for generated stubs", delegate(string value) { _namespaceName = value; }).
                    Add("type=", "type name (including namespace) to stub", delegate(string value) { _typeNames.Add(value); });

                options.Parse(args);

                if (_outputPath == null)
                {
                    Console.WriteLine("No /out?");
                    ShowOptions(options);
                    return 1;
                }

                if (_assemblyPath == null)
                {
                    Console.WriteLine("No /assembly?");
                    ShowOptions(options);
                    return 1;
                }

                if (_typeNames.Count == 0)
                {
                    Console.WriteLine("No /type?");
                    ShowOptions(options);
                    return 1;
                }

                GenerateCodeToFile();

                return 0;
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
                return 1;
            }
        }

        private void ShowOptions(OptionSet options)
        {
            Console.WriteLine();
            options.WriteOptionDescriptions(Console.Out);
            Console.WriteLine();
            Console.WriteLine("/type can be specified more than once if you want to stub more than one type!");
            Console.WriteLine("/type can end with a * to act as a wildcard!");
        }

        private void GenerateCodeToFile()
        {
            WriteEmbeddedResource("Stubify.Header.cs");

            GenerateNamespace();

            string newText = _sb.ToString();

            if (File.Exists(_outputPath))
            {
                string existingText = File.ReadAllText(_outputPath);

                if (existingText == newText)
                {
                    return;
                }

                FileAttributes attributes = File.GetAttributes(_outputPath);

                if ((attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
                {
                    return;
                }
            }

            using (FileStream fs = new FileStream(_outputPath, FileMode.Create))
            using (StreamWriter sw = new StreamWriter(fs))
            {
                sw.Write(newText);
            }
        }

        private void GenerateNamespace()
        {
            WriteLine("namespace {0}", _namespaceName);
            WriteLine("{");
            PushIndent();

            WriteLine("using Internals;");

            foreach (Type type in GetTypes(_typeNames))
            {
                GenerateCode(type);
            }

            WriteEmbeddedResource("Stubify.Internals.cs");

            PopIndent();
            WriteLine("}");
        }

        private void WriteEmbeddedResource(string resourceName)
        {
            using (Stream s = typeof(Program).Assembly.GetManifestResourceStream(resourceName))
            {
                if (s == null)
                {
                    throw new InvalidOperationException();
                }

                using (StreamReader r = new StreamReader(s))
                {
                    string line;
                    while ((line = r.ReadLine()) != null)
                    {
                        WriteLine(line);
                    }
                }
            }
        }

        private IEnumerable<Type> GetTypes(IEnumerable<string> typeNames)
        {
            Dictionary<Type, Type> types = new Dictionary<Type, Type>();

            Assembly assembly = Assembly.LoadFrom(_assemblyPath);

            foreach (string typeName in typeNames)
            {
                if (typeName.EndsWith("*"))
                {
                    string prefix = typeName.Substring(0, typeName.Length - 1);

                    foreach (Type type in assembly.GetTypes())
                    {
                        if (type.IsPublic && !type.IsSealed && type.FullName.StartsWith(prefix))
                        {
                            types[type] = type;
                        }
                    }
                }
                else
                {
                    Type type = assembly.GetType(typeName);

                    if (type == null)
                    {
                        Console.WriteLine("Could not find type {0}!", typeName);
                        Environment.Exit(1);
                    }

                    types[type] = type;
                }
            }

            return types.Keys;
        }

        private void GenerateCode(Type type)
        {
            string newClassName = type.Name + "Stub";

            WriteLine("public class " + newClassName + " : " + type.FullName);
            WriteLine("{");
            PushIndent();

            WriteLine("private readonly StubHelper _stubHelper;");

            string membersClassName = type.Name + "Members";

            WriteLine("private readonly " + membersClassName + " _stub;");

            WriteLine("public {0} Stub {{ get {{ return _stub; }} }}", membersClassName);

            if (type.IsInterface)
            {
                AddConstructor(newClassName, new ParameterInfo[0], membersClassName);
            }

            _methods.Clear();

            AddStubMembers(type, newClassName, membersClassName);

            AddStubClass(type, membersClassName);

            PopIndent();
            WriteLine("}");
        }

        private readonly Dictionary<string, string> _methods = new Dictionary<string, string>();

        private void AddStubMembers(Type type, string stubClassName, string membersClassName)
        {
            foreach (MemberInfo mi in type.GetMembers(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
            {
                if (mi is ConstructorInfo)
                {
                    AddStubConstructor((ConstructorInfo)mi, stubClassName, membersClassName);
                }
                else if (mi is MethodInfo)
                {
                    MethodInfo methodInfo = (MethodInfo)mi;

                    string key = GetMethodKey(methodInfo);

                    AddStubMethod(type, methodInfo, _methods.ContainsKey(key));

                    _methods[key] = key;
                }
                else if (mi is PropertyInfo)
                {
                    AddStubProperty(type, (PropertyInfo)mi);
                }
                else if (mi is EventInfo)
                {
                    AddStubEvent((EventInfo)mi);
                }
            }

            if (type.IsInterface)
            {
                foreach (Type interfaceType in type.GetInterfaces())
                {
                    AddStubMembers(interfaceType, stubClassName, membersClassName);
                }
            }
        }

        private void AddStubConstructor(ConstructorInfo ci, string stubClassName, string membersClassName)
        {
            if (ci.IsPrivate)
            {
                return;
            }

            AddConstructor(stubClassName, ci.GetParameters(), membersClassName);
        }

        private void AddConstructor(string stubClassName, ParameterInfo[] parameters, string membersClassName)
        {
            Write("public {0}(", stubClassName);
            WriteParameterTypesAndNames(parameters);
            Write(")");

            if (parameters != null && parameters.Length > 0)
            {
                Write(" : base(");
                WriteParameterNames(parameters);
                Write(")");
            }

            WriteLine("");
            WriteLine("{");
            PushIndent();
            WriteLine("_stubHelper = new StubHelper();");
            WriteLine("_stub = new {0}(_stubHelper);", membersClassName);
            PopIndent();
            WriteLine("}");
        }

        private void AddStubMethod(Type type, MethodInfo mi, bool methodWithSameNameAlreadyAdded)
        {
            if (mi.IsSpecialName) // Catches property and event methods.
            {
                return;
            }

            if (mi.DeclaringType == typeof(object)) // Catches GetHashCode, GetType, and ToString.
            {
                return;
            }

            if (mi.IsPrivate) // Catches explicit interface implementations.
            {
                return;
            }

            if (!mi.IsVirtual)
            {
                return;
            }

            if (mi.IsFinal)
            {
                return;
            }

            if (HasRefOrOutParams(mi))
            {
                return;
            }

            if (!(type.IsInterface && methodWithSameNameAlreadyAdded))
            {
                if (mi.IsPublic)
                {
                    Write("public ");
                }
                else if (mi.IsFamily)
                {
                    Write("protected ");
                }

                if (!type.IsInterface)
                {
                    Write("override ");
                }
            }

            Write("{0} ", GetCSharpTypeName(mi.ReturnType));

            if (type.IsInterface && methodWithSameNameAlreadyAdded)
            {
                Write("{0}.", GetCSharpTypeName(type));
            }

            Write("{0}(", mi.Name);

            WriteParameterTypesAndNames(mi.GetParameters());

            WriteLine(")");
            WriteLine("{");
            PushIndent();

            if (mi.ReturnType == typeof(void))
            {
                Write("_stubHelper.ExecuteAction");
            }
            else
            {
                Write("return _stubHelper.ExecuteFunc<{0}>", GetCSharpTypeName(mi.ReturnType));
            }

            Write("(\"{0}\"", GetMethodKey(mi));

            foreach (ParameterInfo pi in mi.GetParameters())
            {
                Write(", ");
                Write(pi.Name);
            }

            WriteLine(");");

            PopIndent();
            WriteLine("}");
        }

        private bool HasRefOrOutParams(MethodInfo mi)
        {
            foreach (ParameterInfo pi in mi.GetParameters())
            {
                if (pi.ParameterType.IsByRef || pi.IsOut)
                {
                    return true;
                }
            }

            return false;
        }

        private void AddStubProperty(Type type, PropertyInfo pi)
        {
            MethodInfo getMethod = pi.GetGetMethod(true);
            MethodInfo setMethod = pi.GetSetMethod(true);

            if (!((getMethod != null && getMethod.IsVirtual) || (setMethod != null && setMethod.IsVirtual)))
            {
                return;
            }

            string getAccess = null;
            string setAccess = null;

            if (getMethod != null)
            {
                getAccess = getMethod.IsPublic ? "public " : "protected ";
            }

            if (setMethod != null)
            {
                setAccess = setMethod.IsPublic ? "public " : "protected ";
            }

            if (getAccess == setAccess)
            {
                setAccess = null;
            }

            Write(getAccess ?? setAccess);

            if (type.IsClass)
            {
                Write("override ");
            }

            Write("{0} ", GetCSharpTypeName(pi.PropertyType));

            if (pi.GetIndexParameters().Length > 0)
            {
                Write("this[");
                WriteParameterTypesAndNames(pi.GetIndexParameters());
                Write("]");
            }
            else
            {
                Write(pi.Name);
            }

            WriteLine("");

            WriteLine("{");
            PushIndent();

            if (pi.CanRead)
            {
                Write("get {{ return _stubHelper.ExecutePropertyGetter<{0}>(\"{1}\"", GetCSharpTypeName(pi.PropertyType), GetMethodKey(getMethod));

                if (pi.GetIndexParameters().Length > 0)
                {
                    Write(", ");
                    WriteParameterNames(pi.GetIndexParameters());
                }

                WriteLine("); }");
            }

            if (pi.CanWrite)
            {
                if (setAccess != null && getAccess != null)
                {
                    Write(setAccess);
                }

                Write("set {{ _stubHelper.ExecutePropertySetter(\"{0}\", ", GetMethodKey(setMethod));

                if (pi.CanRead)
                {
                    Write("\"{0}\"", GetMethodKey(getMethod));
                }
                else
                {
                    Write("null");
                }

                Write(", value");

                if (pi.GetIndexParameters().Length > 0)
                {
                    Write(", ");
                    WriteParameterNames(pi.GetIndexParameters());
                }

                WriteLine("); }");
            }

            PopIndent();
            WriteLine("}");
        }

        private void WriteParameterTypesAndNames(IEnumerable<ParameterInfo> parameters)
        {
            bool first = true;

            foreach (ParameterInfo pi in parameters)
            {
                if (!first)
                {
                    Write(", ");
                }

                Write("{0} {1}", GetCSharpTypeName(pi.ParameterType), pi.Name);

                first = false;
            }
        }

        private void WriteParameterNames(IEnumerable<ParameterInfo> parameters)
        {
            bool first = true;

            foreach (ParameterInfo pi in parameters)
            {
                if (!first)
                {
                    Write(", ");
                }

                Write(pi.Name);

                first = false;
            }
        }

        private void AddStubEvent(EventInfo ei)
        {
            // TODO: Finish this method to record handlers. Also add a method to raise the event.
            WriteLine("public event {0} {1}", GetCSharpTypeName(ei.EventHandlerType), ei.Name);
            WriteLine("{");
            PushIndent();
            WriteLine("add { }");
            WriteLine("remove { }");
            PopIndent();
            WriteLine("}");
        }

        private void AddStubClass(Type type, string stubClassName)
        {
            WriteLine("public class {0}", stubClassName);
            WriteLine("{");
            PushIndent();
            WriteLine("private readonly StubHelper _stubHelper;");
            WriteLine("internal {0}(StubHelper stubHelper) {{ _stubHelper = stubHelper; }}", stubClassName);
            _methods.Clear();
            AddMembers(type);
            PopIndent();
            WriteLine("}");
        }

        private void AddMembers(Type type)
        {
            foreach (MemberInfo mi in type.GetMembers(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
            {
                if (mi is MethodInfo)
                {
                    MethodInfo methodInfo = (MethodInfo)mi;

                    string key = GetMethodKey(methodInfo);

                    if (_methods.ContainsKey(key))
                    {
                        continue;
                    }

                    AddMethod(type, methodInfo);

                    _methods.Add(key, key);
                }
            }

            if (type.IsInterface)
            {
                foreach (Type interfaceType in type.GetInterfaces())
                {
                    AddMembers(interfaceType);
                }
            }
        }

        private void AddMethod(Type type, MethodInfo mi)
        {
            if (mi.DeclaringType == typeof(object)) // Catches GetHashCode, GetType, and ToString.
            {
                return;
            }

            if (mi.IsPrivate) // Catches explicit interface implementations.
            {
                return;
            }

            if (!mi.IsVirtual)
            {
                return;
            }

            if (mi.IsFinal)
            {
                return;
            }

            if (HasRefOrOutParams(mi))
            {
                return;
            }

            string methodType = mi.ReturnType == typeof(void) ? "ActionMethod" : "FuncMethod";

            if (mi.GetParameters().Length > 0 || mi.ReturnType != typeof(void))
            {
                methodType += "<";
            }

            bool first = true;

            foreach (ParameterInfo pi in mi.GetParameters())
            {
                if (!first)
                {
                    methodType += ", ";
                }

                methodType += GetCSharpTypeName(pi.ParameterType);

                first = false;
            }

            if (mi.GetParameters().Length > 0 || mi.ReturnType != typeof(void))
            {
                if (mi.ReturnType != typeof(void))
                {
                    if (mi.GetParameters().Length > 0)
                    {
                        methodType += ", ";
                    }

                    methodType += GetCSharpTypeName(mi.ReturnType);
                }

                methodType += ">";
            }

            if (!IsOverloaded(type, mi))
            {
                WriteLine("public {0} {1} {{ get {{ return new {0}(_stubHelper, \"{2}\"); }} }}", methodType, mi.Name, GetMethodKey(mi));
            }
            else
            {
                Write("public {0} {1}(", methodType, mi.Name);

                if (mi.GetParameters().Length > 0)
                {
                    Write("Args<");

                    first = true;

                    foreach (ParameterInfo pi in mi.GetParameters())
                    {
                        if (!first)
                        {
                            Write(", ");
                        }

                        Write(GetCSharpTypeName(pi.ParameterType));

                        first = false;
                    }

                    Write("> args");
                }

                WriteLine(") {{ return new {0}(_stubHelper, \"{2}\"); }}", methodType, mi.Name, GetMethodKey(mi));
            }
        }

        private string GetMethodKey(MethodInfo mi)
        {
            string key = mi.Name + "(";

            bool first = true;

            foreach (ParameterInfo pi in mi.GetParameters())
            {
                if (!first)
                {
                    key += ", ";
                }

                key += GetCSharpTypeName(pi.ParameterType);

                first = false;
            }

            key += ")";

            return key;
        }

        private bool IsOverloaded(Type type, MethodInfo mi)
        {
            int count = 0;

            foreach (MethodInfo mi2 in type.GetMethods())
            {
                if (mi2.Name == mi.Name)
                {
                    count++;
                }
            }

            return count > 1;
        }

        private string GetCSharpTypeName(Type type)
        {
            switch (type.FullName)
            {
                case "System.Boolean": return "bool";
                case "System.Byte": return "byte";
                case "System.Char": return "char";
                case "System.Decimal": return "decimal";
                case "System.Double": return "double";
                case "System.Int16": return "short";
                case "System.Int32": return "int";
                case "System.Int64": return "long";
                case "System.SByte": return "sbyte";
                case "System.Single": return "float";
                case "System.String": return "string";
                case "System.UInt16": return "ushort";
                case "System.UInt32": return "uint";
                case "System.UInt64": return "ulong";
                case "System.Void": return "void";
            }

            if (!type.IsGenericType)
            {
                return type.FullName;
            }

            string typeName = type.FullName.Substring(0, type.FullName.IndexOf("`")) + "<";

            bool first = true;

            foreach (Type typeArg in type.GetGenericArguments())
            {
                if (!first)
                {
                    typeName += ", ";
                }

                typeName += GetCSharpTypeName(typeArg);

                first = false;
            }

            typeName += ">";

            return typeName;
        }

        #region Write helpers

        private readonly StringBuilder _sb = new StringBuilder();
        readonly List<string> _indents = new List<string>();
        string _currentIndent = "";
        bool _needsIndent;

        private void Write(string text)
        {
            if (string.IsNullOrEmpty(text))
            {
                return;
            }

            if (_needsIndent)
            {
                _sb.Append(_currentIndent);
                _needsIndent = false;
            }

            _sb.Append(text);
        }

        private void Write(string format, params object[] args)
        {
            Write(string.Format(format, args));
        }

        private void WriteLine(string text)
        {
            Write(text);

            _sb.AppendLine();

            _needsIndent = true;
        }

        private void WriteLine(string format, params object[] args)
        {
            WriteLine(string.Format(format, args));
        }

        private void PushIndent(string indent)
        {
            _indents.Add(indent);
            _currentIndent = string.Join("", _indents.ToArray());
        }

        private void PopIndent()
        {
            _indents.RemoveAt(_indents.Count - 1);
            _currentIndent = string.Join("", _indents.ToArray());
        }

        private void PushIndent()
        {
            PushIndent("    ");
        }

        #endregion
    }
}
